import 'dart:io';
import 'dart:isolate';

import 'package:image/image.dart' as imageLib;

import 'package:camera/camera.dart';
import 'package:google_ml_kit/google_ml_kit.dart';
import 'package:path_provider/path_provider.dart';
import 'package:tflite_flutter_helper/tflite_flutter_helper.dart';

import 'edge_detection/isolate_utils.dart';
import 'tflite_flutter/classifier.dart';

class MLVService {
  static final MLVService _singleton = MLVService._internal();
  factory MLVService() {
    return _singleton;
  }
  MLVService._internal();
  Classifier _classifier = Classifier();
  bool isBusy = false;

  IsolateUtils isolateUtils;

  Future<void> init() async {
    isBusy = false;
    isolateUtils = IsolateUtils();
    await isolateUtils.start();
  }

  void end() {
    isolateUtils.end();
  }

  Future<String> onOCRResult(String imagePath) async {
    TextDetector detector = GoogleMlKit.instance.textDetector();
    RecognisedText labels;
    try {
      labels = await detector.processImage(InputImage.fromFilePath(imagePath));
    } catch (e) {
      print(e);
    }
    List<TextLine> tempLines = [];
    List<List<TextLine>> tempLinesList = [];

    //stored all textline in tempLines
    labels.textBlocks.forEach((element) {
      tempLines.addAll(element.textLines);
    });

    //sorted result by text height
    tempLines.sort((a, b) => a.lineRect.top.compareTo(b.lineRect.top));

    //list of textline for a same line
    List<TextLine> tempData = [];
    for (var i = 0; i < tempLines.length; i++) {
      TextLine textLine = tempLines[i];

      if (i == 0) {
        tempData.add(textLine);
      } else {
        double center = (textLine.lineRect.top + textLine.lineRect.bottom) / 2;
        TextLine prevTextLine = tempLines[i - 1];
        double prevCenter =
            (prevTextLine.lineRect.top + prevTextLine.lineRect.bottom) / 2;
        double prevTolerance = (prevCenter - prevTextLine.lineRect.top);
        bool sameLine = (center - prevCenter) <=
            prevTolerance * 1.10; // additional tolerance;

        if (sameLine) {
          tempData.add(textLine);
        } else {
          tempLinesList.add(List.from(tempData));
          tempData.clear();
          tempData.add(textLine);
        }
      }
    }
    String combinedString = "";
    tempLinesList.forEach((result) {
      //sorted result from left to right
      result.sort((a, b) => a.lineRect.left.compareTo(b.lineRect.left));

      for (var i = 0; i < result.length; i++) {
        TextLine line = result[i];

        if (i > 0) combinedString += " ";

        combinedString += line.lineText;

        //end of loop, add string to next line
        if (result.length == i + 1) combinedString += "\n";
      }
    });
    // print(combinedString);
    return combinedString;
  }

  Future<bool> isCaptureClear(String imagePath) async {
    TextDetector detector = GoogleMlKit.instance.textDetector();
    RecognisedText labels;
    try {
      labels = await detector.processImage(InputImage.fromFilePath(imagePath));
    } catch (e) {
      print(e);
    }
    if (labels?.text == "") {
      return false;
    } else {
      return true;
    }
  }

  /// Callback to receive each frame [CameraImage] perform inference on it
  Future<CategoryWithEdge> onClassifiedResult(CameraImage cameraImage) async {
    if (_classifier.interpreter != null && _classifier.labels != null) {
      // If previous inference has not completed then return
      if (isBusy) {
        return null;
      }

      isBusy = true;
      Directory directory = await getTemporaryDirectory();
      var uiThreadTimeStart = DateTime.now().millisecondsSinceEpoch;

      // Data to be passed to inference isolate
      var isolateData = IsolateData(cameraImage, directory.path,
          _classifier.interpreter.address, _classifier.labels);

      // We could have simply used the compute method as well however
      // it would be as in-efficient as we need to continuously passing data
      // to another isolate.

      /// perform inference in separate isolate
      CategoryWithEdge inferenceResults = await inference(isolateData);

      var uiThreadInferenceElapsedTime =
          DateTime.now().millisecondsSinceEpoch - uiThreadTimeStart;

      // set predicting to false to allow new frames

      isBusy = false;
      return inferenceResults;
    } else {
      return null;
    }
  }

  /// Runs inference in another isolate
  Future<CategoryWithEdge> inference(IsolateData isolateData) async {
    ReceivePort responsePort = ReceivePort();
    if (isolateUtils.sendPort == null) return null;
    isolateUtils.sendPort
        .send(isolateData..responsePort = responsePort.sendPort);
    var results = await responsePort.first;
    return results;
  }
}
